package com.ihr360.expr.engine;

import com.ihr360.expr.ExprError;
import com.ihr360.expr.ExprException;
import com.ihr360.expr.context.IEvaluationContext;
import com.ihr360.expr.core.Expr;
import com.ihr360.expr.core.ExprEvaluatable;
import com.ihr360.expr.core.ExprVariable;
import com.ihr360.expr.core.GraphCycleException;
import com.ihr360.expr.core.graph.Edge;
import com.ihr360.expr.core.graph.Graph;
import com.ihr360.expr.parser.IParserVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Iterator;

public class DependencyEngine extends AbstractCalculationEngine implements
        IParserVisitor, IEvaluationContext, GraphTraversalListener {
    final Logger logger = LoggerFactory.getLogger(DependencyEngine.class.getName());
    protected Graph graph = new Graph();

    public DependencyEngine(EngineProvider provider) {
        super(provider);
        this.graph.setIncludeEdges(false);
    }


    @Override
    protected void set(VariableID variableID, String expression) throws ExprException {
        if (logger.isDebugEnabled()) {
            logger.info("set variableId: {} expression:{}", variableID, expression);
        }
        // If null then remove all references
        if (expression == null) {
            rawInputs.remove(variableID);
            removeEvaluatedValue(variableID);
            inputs.remove(variableID);
            updateDependencies(variableID, null);
            return;
        }

        rawInputs.put(variableID, expression);

        Expr expr = parseExpression(variableID, expression);

        // Update the dependency graph
        updateDependencies(variableID, expr);

        // Set the inputs
        provider.inputChanged(variableID, expr);
        inputs.put(variableID, expr);

        if (autoCalculate) {
            // Always evaluate the expression entered
            if (expr.evaluatable) {
                Expr eval = ((ExprEvaluatable) expr).evaluate(this);
                provider.valueChanged(variableID, eval);
                addEvaluatedValue(variableID, eval);
            } else {
                provider.valueChanged(variableID, expr);
                addEvaluatedValue(variableID, expr);
            }
        } else {
            provider.valueChanged(variableID, expr);
            addEvaluatedValue(variableID, expr);
        }


        // Recalculate the dependencies if required
        if (autoCalculate) {
            graph.traverse(variableID, this);
        }
    }

    @Override
    public void calculate(boolean force) throws ExprException {
        if (autoCalculate && !force) {
            return;
        }

        graph.sort();
        Iterator i = graph.iterator();
        while (i.hasNext()) {
            VariableID variableID = (VariableID) i.next();
            Expr input = inputs.get(variableID);
            if (input instanceof ExprEvaluatable) {
                Expr eval = ((ExprEvaluatable) input).evaluate(this);
                if (eval instanceof ExprError) {
                    logger.error(variableID + " " + eval.toString());
                    provider.onError(variableID,input,(ExprError)eval);
                    continue;
                }
                provider.valueChanged(variableID, eval);
                addEvaluatedValue(variableID, eval);
            }
        }
    }


    private void updateDependencies(VariableID variableID, Expr expr)
            throws ExprException {
        graph.clearInbounds(variableID);
        ExprVariable[] vars = ExprVariable.findVariables(expr);
        for (ExprVariable var : vars) {
            VariableID source = (VariableID) var.getAnnotation();
            try {
                addDependencies(source, variableID);
            } catch (GraphCycleException ex) {
                for (ExprVariable v : vars) {
                    removeDependencies((VariableID) v.getAnnotation(), variableID);
                }
                throw new ExprException(ex);
            }
        }
    }

    private void addDependencies(VariableID source, VariableID target)
            throws GraphCycleException {
        graph.add(new Edge(source, target));
    }

    private void removeDependencies(VariableID source, VariableID target) {
        graph.remove(new Edge(source, target));
    }


    @Override
    public void traverse(VariableID node) {
        VariableID id = node;
        Expr input = inputs.get(id);
        if (input instanceof ExprEvaluatable) {
            try {
                Expr eval = ((ExprEvaluatable) input).evaluate(this);
                provider.valueChanged(id, eval);
                addEvaluatedValue(id, eval);
            } catch (ExprException e) {
                e.printStackTrace();
            }
        }
    }
}